#region (c) 2014 THN Solutions LLC. - All rights reserved
/*
THN DATA ABSTRACTION LAYER version 1

Copyright (c) 2014, THN Solutions LLC. ( www.ThnSolutions.com )
Author: Nguyễn, M. Hải                 ( www.Minh-Hai.com     )
All rights reserved.

This library is dual-licensed.
  + For commercial software, please obtain a commercial license from THN Solutions LLC.

  + For free software, this library is licensed under GPL version 3. A summary of GPLv3 is
    listed below. You should also find a copy of GPLv3 as file License_GPLv3.txt included
	with the source-code files.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 3 as published by
    the Free Software Foundation  of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#endregion

#region Using directives
using System;
using System.IO;
using System.Xml;
using System.Collections;
using System.Reflection;
using System.ComponentModel;
using Thn.Application;
using Thn.Reflection;
#endregion

namespace Thn.Data.Settings
{
    /// <summary>
    /// This is a helper class to deserialize objects from xml file. The object is subpose to have one parent to mimic tree-structure.
    /// </summary>
    public class NestedObjectLoader
    {        
        #region Id Name
        private string mIdName = "ID";
        /// <summary>
        /// Gets/Sets name of the ID property
        /// </summary>
        public string IdName
        {
            get { return mIdName; }
            set { mIdName = value; }
        }
        #endregion

        #region Parent Id Name
        private string mParentIdName = "ParentID";
        /// <summary>
        /// Gets/Sets name of the ParentID property
        /// </summary>
        public string ParentIdName
        {
            get { return mParentIdName; }
            set { mParentIdName = value; }
        }
        #endregion

        #region Filename
        private string mFilename = string.Empty;
        /// <summary>
        /// Gets/Sets name of the xml file to load
        /// </summary>
        public string Filename
        {
            get { return mFilename; }
            set { mFilename = value; }
        }
        #endregion

        #region Object Type
        private Type mObjectType;
        /// <summary>
        /// Gets/Sets type of object to load
        /// </summary>
        public Type ObjectType
        {
            get { return mObjectType; }
            set { mObjectType = value; }
        }
        #endregion	

        #region Results
        private IList mResults = null;
        /// <summary>
        /// Gets list of parsed items
        /// </summary>
        public IList Results
        {
            get { return mResults; }
        }
        #endregion
	
        #region Load
        /// <summary>
        /// Read xml file and parse result
        /// </summary>
        /// <param name="filename">Name of the xml file to load</param>
        public void Load(string filename)
        {
            mFilename = filename;
            Load();
        }

        /// <summary>
        /// Read xml file and parse result
        /// </summary>
        public void Load()
        {
            if (mObjectType == null) Thn.Exceptions.NullOrEmptyException.Publish("ObjectType");
            if (File.Exists(mFilename))
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(mFilename);
                Parse(doc);
            }
            else Thn.Exceptions.FileNotFoundException.Publish(mFilename);
        }
        #endregion

        #region Parsing
        #region Parse Document
        void Parse(XmlDocument doc)
        {
            mResults = new ArrayList();

            //parse children
            for (int i = 0; i < doc.ChildNodes.Count; i++)
            {
                if (doc.ChildNodes[i] is XmlElement)
                {
                    ParseChildren((XmlElement)doc.ChildNodes[i], null);
                }
            }
        }
        #endregion

        #region Clone Properties
        MemberInfoCollection mis = null;

        /// <summary>
        /// Copy properties from parent to child
        /// </summary>
        void CloneProperties(object parent, object child)
        {
            //reflect saveble members
            if (mis == null)
            {
                ObjectReflector reflector = new ObjectReflector();
                mis = reflector.GetMembers(mObjectType, true, true, typeof(SavableAttribute), false, true);
            }

            //clone
            string id = mIdName.ToUpper();
            for (int i = 0; i < mis.Count; i++)
            {
                
                MemberInfo mi = mis[i];
                if (mi.Name.ToUpper() != id)
                {
                    if (mi is PropertyInfo)
                    {
                        PropertyInfo pi = (PropertyInfo)mi;
                        object value = pi.GetValue(parent, null);
                        pi.SetValue(child, value, null);
                    }
                    else if (mi is FieldInfo)
                    {
                        FieldInfo fi = (FieldInfo)mi;
                        object value = fi.GetValue(parent);
                        fi.SetValue(child, value);
                    }
                }
            }
        }
        #endregion

        #region Parse Children
        void ParseChildren(XmlElement element, object parent)
        {
            //parse children
            for (int i = 0; i < element.ChildNodes.Count; i++)
            {
                if (element.ChildNodes[i] is XmlElement)
                {
                    XmlElement child = (XmlElement)element.ChildNodes[i];
                    ParseElement(child, parent);
                }
            }
        }
        #endregion

        #region Parse Element
        /// <summary>
        /// Parse XmlElement to build an item.
        /// </summary>
        /// <param name="element">The XmlElement containing attributes to build one item</param>
        /// <param name="parent">If not null, clone parent properties to child</param>
        void ParseElement(XmlElement element, object parent)
        {
            object item = Activator.CreateInstance(mObjectType);
            FieldInfo fi = null;
            PropertyInfo pi = null;
            object data = null;

            //clone parent properties
            if (parent != null)
            {
                CloneProperties(parent, item);
                
                //get parent's ID
                data = null;
                pi = mObjectType.GetProperty(mIdName, BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Public);
                if (pi != null)
                {
                    data = pi.GetValue(parent, null);
                }
                else
                {
                    fi = mObjectType.GetField(mIdName, BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Public);
                    if (fi != null) data = fi.GetValue(parent);
                }

                //set item's parentID
                pi = mObjectType.GetProperty(mParentIdName, BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Public);
                if (pi != null)
                {
                    pi.SetValue(item, data, null);
                }
                else
                {
                    fi = mObjectType.GetField(mParentIdName, BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Public);
                    if (fi != null) fi.SetValue(item, data);
                }
            }

            //parse attributes
            for (int i = 0; i < element.Attributes.Count; i++)
            {
                XmlAttribute att = element.Attributes[i];

                //find member with such name
                Type memberType = null;
                pi = mObjectType.GetProperty(att.Name, BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Public);
                if (pi != null)
                {
                    memberType = pi.PropertyType;
                }
                else
                {
                    fi = mObjectType.GetField(att.Name, BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Public);
                    if (fi != null) memberType = fi.FieldType;
                }

                //convert data                
                if (memberType != null)
                {
                    TypeConverter converter = TypeDescriptor.GetConverter(memberType);
                    data = converter.ConvertFromString(att.Value);
                }
                else Thn.Exceptions.MemberNotFoundException.Publish(att.Name);

                //assign value
                if (data != null)
                {
                    if (pi != null) pi.SetValue(item, data, null);
                    else if (fi != null) fi.SetValue(item, data);
                }
            }            

            //add to results
            mResults.Add(item);

            //parse children
            ParseChildren(element, item);
        }
        #endregion        
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public NestedObjectLoader()
        { }
        #endregion
    }
}
